package eth

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"time"

	"github.com/ledgerwatch/erigon-lib/common"
	libcommon "github.com/ledgerwatch/erigon-lib/common"
	"github.com/ledgerwatch/erigon-lib/kv"
	"github.com/ledgerwatch/erigon/core"
	"github.com/ledgerwatch/erigon/core/state"
	slimarchive "github.com/ledgerwatch/erigon/core/state/slim_archive"
	"github.com/ledgerwatch/erigon/core/systemcontracts"
	"github.com/ledgerwatch/erigon/core/types"
	"github.com/ledgerwatch/erigon/core/vm"
	"github.com/ledgerwatch/erigon/core/vm/evmtypes"
	"github.com/ledgerwatch/erigon/eth/stagedsync"
	"github.com/ledgerwatch/erigon/ethdb/pebble"
	"github.com/ledgerwatch/log/v3"
	"github.com/schollz/progressbar/v3"
)

func (s *Ethereum) SetSlimArchive(db *pebble.Database) {
	s.slimArchiveDB = db
}

func (s *Ethereum) ComputeTxEnv(ctx context.Context, block *types.Block, dbtx kv.Tx, txIndex int) (*state.IntraBlockState, error) {
	var reader state.StateReader
	if s.slimArchiveDB != nil {
		return state.New(slimarchive.NewSlimReader(block.NumberU64(), uint16(txIndex), s.slimArchiveDB)), nil
	} else {
		reader = state.NewPlainState(dbtx, block.NumberU64(), systemcontracts.SystemContractCodeLookup[s.chainConfig.ChainName])
	}
	statedb := state.New(reader)
	if txIndex == 0 && len(block.Transactions()) == 0 {
		return statedb, nil
	}

	// Recompute transactions up to the target index.
	signer := types.MakeSigner(s.chainConfig, block.NumberU64(), block.Time())
	getHeader := func(hash libcommon.Hash, n uint64) *types.Header {
		h, _ := s.blockReader.HeaderByNumber(ctx, dbtx, n)
		return h
	}
	header := block.HeaderNoCopy()
	blockContext := core.NewEVMBlockContext(header, core.GetHashFn(header, getHeader), s.engine, nil)
	vmenv := vm.NewEVM(blockContext, evmtypes.TxContext{}, statedb, s.chainConfig, vm.Config{})
	rules := vmenv.ChainRules()

	consensusHeaderReader := stagedsync.NewChainReaderImpl(s.chainConfig, dbtx, nil)

	core.InitializeBlockExecution(s.engine, consensusHeaderReader, header, block.Transactions(), block.Uncles(), s.chainConfig, statedb)

	for idx, txn := range block.Transactions() {
		select {
		default:
		case <-ctx.Done():
			return nil, ctx.Err()
		}
		statedb.SetTxContext(txn.Hash(), block.Hash(), idx)

		// Assemble the transaction call message and return if the requested offset
		msg, _ := txn.AsMessage(*signer, block.BaseFee(), rules)
		if msg.FeeCap().IsZero() && s.engine != nil {
			syscall := func(contract libcommon.Address, data []byte) ([]byte, error) {
				return core.SysCallContract(contract, data, s.chainConfig, statedb, header, s.engine, true /* constCall */)
			}
			msg.SetIsFree(s.engine.IsServiceTransaction(msg.From(), syscall))
		}

		TxContext := core.NewEVMTxContext(msg)
		if idx == txIndex {
			return statedb, nil
		}
		vmenv.Reset(TxContext, statedb)
		// Not yet the searched for transaction, execute on top of the current state
		if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(txn.GetGas()).AddDataGas(txn.GetDataGas()), true /* refunds */, false /* gasBailout */); err != nil {
			return nil, fmt.Errorf("transaction %x failed: %w", txn.Hash(), err)
		}
		// Ensure any modifications are committed to the state
		// Only delete empty objects if EIP161 (part of Spurious Dragon) is in effect
		_ = statedb.FinalizeTx(rules, reader.(*state.PlainState))

		if idx+1 == len(block.Transactions()) {
			// Return the state from evaluating all txs in the block, note no msg or TxContext in this case
			return statedb, nil
		}
	}
	return nil, fmt.Errorf("transaction index %d out of range for block %x", txIndex, block.Hash())
}

// func (s *Ethereum)

func (s *Ethereum) blocksByRange(start, end uint64, maxWorkers int) (blocks types.Blocks, err error) {
	if start == 0 {
		start = 1
	}
	totalTasks := int(end - start)
	taskCh := make(chan uint64, totalTasks)
	go func() {
		for i := start; i < end; i++ {
			taskCh <- i
		}
		close(taskCh)
	}()

	resCh := make(chan *types.Block, totalTasks)

	fetchWorker := func() {
		tx, err := s.chainDB.BeginRo(context.Background())
		if err != nil {
			panic(err)
		}
		defer tx.Rollback()
		for task := range taskCh {
			blk, err := s.blockReader.BlockByNumber(context.Background(), tx, task)
			if err != nil {
				panic(err)
			}
			if blk == nil {
				panic(fmt.Errorf("block %v not found", start))
			}
			resCh <- blk
		}
	}

	for i := 0; i < maxWorkers; i++ {
		go fetchWorker()
	}
	blocks = make(types.Blocks, 0, totalTasks)

	for i := 0; i < totalTasks; i++ {
		blocks = append(blocks, <-resCh)
	}
	return
}

func (s *Ethereum) SampleMidBlockTaskByGasUsed(start, end, interval uint64) (types.Blocks, error) {
	log.Info("sample blocks by mid gas used", "start", start, "end", end, "interval", interval)
	if interval < 1 {
		return nil, errors.New("interval must > 0")
	} else if interval == 1 {
		return s.blocksByRange(start, end, 100)
	}
	bar := progressbar.Default(int64((end - start) / interval))
	sampled := make(types.Blocks, 0)
	t0 := time.Now()
	for i := start; i < end; i += interval {
		bar.Add(1)
		blocks, err := s.blocksByRange(i, i+interval, 100)
		if err != nil {
			return nil, err
		}
		sort.SliceStable(blocks, func(i, j int) bool {
			if blocks[i].GasUsed() != blocks[j].GasUsed() {
				return blocks[i].GasUsed() < blocks[j].GasUsed()
			}
			return blocks[i].NumberU64() < blocks[j].NumberU64()
		})
		sampled = append(sampled, blocks[interval/2])
		// ch <- &BlockTask{Number: blocks[interval/2].NumberU64(), Block: blocks[interval/2]}
	}
	log.Info("sample fetch mid block by gas used finished", "cost", time.Since(t0), "total", len(sampled))
	return sampled, nil
}

func (s *Ethereum) blockTxWorker(id int, taskCh <-chan *types.Block, resultCh chan<- *core.BlockTaskResult) {
	// state.EnabledExpensive = true
	engine := s.engine
	var tx2 kv.Tx
	for block := range taskCh {
		chainConfig := s.chainConfig
		detailTxMetrics := make([]*core.DetailTxMetrics, len(block.Transactions()))
		pstart := time.Now()
		tx, err := s.chainDB.BeginRo(context.Background())
		if err != nil {
			log.Error(err.Error())
			resultCh <- &core.BlockTaskResult{
				Err: err,
			}
			tx.Rollback()
			continue
		}

		getHeader := func(hash libcommon.Hash, n uint64) *types.Header {
			h, _ := s.blockReader.HeaderByNumber(context.Background(), tx, n)
			return h
		}
		var (
			blockCtx = core.NewEVMBlockContext(block.Header(), core.GetHashFn(block.Header(), getHeader), engine, nil)
			signer   = types.MakeSigner(chainConfig, block.NumberU64(), block.Time())
			rules    = chainConfig.Rules(block.NumberU64(), block.Time())
			txns     = block.Transactions()
			usedGas  = uint64(0)
		)
		for idx, txn := range txns {
			// Generate the next state snapshot fast without tracing
			txStart := time.Now()
			if s.slimArchiveDB == nil {
				tx2, err = s.chainDB.BeginRo(context.Background())
				if err != nil {
					log.Error(err.Error())
					resultCh <- &core.BlockTaskResult{
						Err: err,
					}
					tx.Rollback()
					continue
				}
			}
			ibs, err := s.ComputeTxEnv(context.Background(), block, tx2, idx)
			prepareCost := time.Since(txStart)
			if err != nil {
				log.Error(err.Error())
				resultCh <- &core.BlockTaskResult{
					Err: err,
				}
				tx.Rollback()
				continue
			}
			txStart = time.Now()
			ibs.SetTxContext(txn.Hash(), block.Hash(), idx)
			msg, _ := txn.AsMessage(*signer, block.BaseFee(), rules)

			if msg.FeeCap().IsZero() && engine != nil {
				syscall := func(contract common.Address, data []byte) ([]byte, error) {
					return core.SysCallContract(contract, data, chainConfig, ibs, block.Header(), engine, true /* constCall */)
				}
				msg.SetIsFree(engine.IsServiceTransaction(msg.From(), syscall))
			}

			txCtx := evmtypes.TxContext{
				TxHash:   txn.Hash(),
				Origin:   msg.From(),
				GasPrice: msg.GasPrice(),
			}

			vmenv := vm.NewEVM(blockCtx, txCtx, ibs, chainConfig, vm.Config{})
			var refunds = true
			execResult, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()).AddDataGas(msg.DataGas()), refunds, false /* gasBailout */)

			if err != nil {
				log.Error(err.Error(), "block", block.NumberU64(), "index", idx)
				resultCh <- &core.BlockTaskResult{Err: err}
				return
			}
			if s.slimArchiveDB == nil {
				tx2.Rollback()
			}

			usedGas += execResult.UsedGas

			detailTxMetrics[idx] = &core.DetailTxMetrics{
				Index:       idx,
				PrepareCost: prepareCost,
				ProcessTime: time.Since(txStart),
			}
			if state.EnabledExpensive {
				stateRead := ibs.AccountReads + ibs.StorageReads + ibs.CodeReads + ibs.IncarReads
				stateReadCnt := ibs.AccountReadCnt + ibs.StorageReadCnt + ibs.CodeReadCnt + ibs.IncarReadCnt
				detailTxMetrics[idx].ProcessDetail = core.TxProcessDetail{
					ExecTime:      detailTxMetrics[idx].ProcessTime - stateRead,
					StateReadTime: stateRead,
					StateReadCnt:  uint64(stateReadCnt),
				}
			}

			// Finalize the state so any modifications are written to the trie
			// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
			// if err == nil {
			// 	err = ibs.FinalizeTx(rules, state.NewNoopWriter())
			// }
			if err != nil {
				resultCh <- &core.BlockTaskResult{Err: err}
				return
			}
		}
		ptime := time.Since(pstart)
		tx.Rollback()
		if usedGas != block.GasUsed() {
			resultCh <- &core.BlockTaskResult{Err: fmt.Errorf("unmatched gasUsed, block: %v, origin: %v, result: %v", block.NumberU64(), block.GasUsed(), usedGas)}
		} else {
			resultCh <- &core.BlockTaskResult{
				Block: block,
				ProcessCost: core.ProcessCost{
					ProcessTime: ptime,
				},
				DetailTxMetrics: detailTxMetrics,
			}
		}
	}
}

func (s *Ethereum) blockWorker(id int, taskCh <-chan *types.Block, resultCh chan<- *core.BlockTaskResult) {
	// state.EnabledExpensive = true
	engine := s.engine
	for block := range taskCh {
		chainConfig := s.chainConfig
		detailTxMetrics := make([]*core.DetailTxMetrics, len(block.Transactions()))
		var lastStateRead time.Duration
		var lastStateReadCnt uint64
		pstart := time.Now()
		tx, err := s.chainDB.BeginRo(context.Background())
		if err != nil {
			log.Error(err.Error())
			resultCh <- &core.BlockTaskResult{
				Err: err,
			}
			tx.Rollback()
			continue
		}
		ibs, err := s.ComputeTxEnv(context.Background(), block, tx, 0)
		if err != nil {
			log.Error(err.Error())
			resultCh <- &core.BlockTaskResult{
				Err: err,
			}
			tx.Rollback()
			continue
		}
		getHeader := func(hash libcommon.Hash, n uint64) *types.Header {
			h, _ := s.blockReader.HeaderByNumber(context.Background(), tx, n)
			return h
		}
		var (
			blockCtx = core.NewEVMBlockContext(block.Header(), core.GetHashFn(block.Header(), getHeader), engine, nil)
			signer   = types.MakeSigner(chainConfig, block.NumberU64(), block.Time())
			rules    = chainConfig.Rules(block.NumberU64(), block.Time())
			txns     = block.Transactions()
			usedGas  = uint64(0)
		)
		for idx, txn := range txns {
			// Generate the next state snapshot fast without tracing
			lastStateRead = ibs.AccountReads + ibs.StorageReads + ibs.CodeReads + ibs.IncarReads
			lastStateReadCnt = uint64(ibs.AccountReadCnt + ibs.StorageReadCnt + ibs.CodeReadCnt + ibs.IncarReadCnt)
			txStart := time.Now()

			ibs.SetTxContext(txn.Hash(), block.Hash(), idx)
			msg, _ := txn.AsMessage(*signer, block.BaseFee(), rules)

			if msg.FeeCap().IsZero() && engine != nil {
				syscall := func(contract common.Address, data []byte) ([]byte, error) {
					return core.SysCallContract(contract, data, chainConfig, ibs, block.Header(), engine, true /* constCall */)
				}
				msg.SetIsFree(engine.IsServiceTransaction(msg.From(), syscall))
			}

			txCtx := evmtypes.TxContext{
				TxHash:   txn.Hash(),
				Origin:   msg.From(),
				GasPrice: msg.GasPrice(),
			}

			vmenv := vm.NewEVM(blockCtx, txCtx, ibs, chainConfig, vm.Config{})
			var refunds = true
			execResult, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()).AddDataGas(msg.DataGas()), refunds, false /* gasBailout */)

			if err != nil {
				log.Error(err.Error(), "block", block.NumberU64(), "index", idx)
				resultCh <- &core.BlockTaskResult{Err: err}
				return
			}

			usedGas += execResult.UsedGas

			detailTxMetrics[idx] = &core.DetailTxMetrics{
				Index:       idx,
				PrepareCost: txStart.Sub(pstart),
				ProcessTime: time.Since(txStart),
			}
			if state.EnabledExpensive {
				totalStateRead := ibs.AccountReads + ibs.StorageReads + ibs.CodeReads + ibs.IncarReads
				totalStateReadCnt := ibs.AccountReadCnt + ibs.StorageReadCnt + ibs.CodeReadCnt + ibs.IncarReadCnt
				detailTxMetrics[idx].ProcessDetail = core.TxProcessDetail{
					ExecTime:      detailTxMetrics[idx].ProcessTime - (totalStateRead - lastStateRead),
					StateReadTime: (totalStateRead - lastStateRead),
					StateReadCnt:  uint64(totalStateReadCnt) - lastStateReadCnt,
				}
			}

			// Finalize the state so any modifications are written to the trie
			// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
			if err == nil {
				err = ibs.FinalizeTx(rules, state.NewNoopWriter())
			}
			if err != nil {
				resultCh <- &core.BlockTaskResult{Err: err}
				return
			}
		}
		ptime := time.Since(pstart)
		tx.Rollback()
		if usedGas != block.GasUsed() {
			resultCh <- &core.BlockTaskResult{Err: fmt.Errorf("unmatched gasUsed, block: %v, origin: %v, result: %v", block.NumberU64(), block.GasUsed(), usedGas)}
		} else {
			resultCh <- &core.BlockTaskResult{
				Block: block,
				ProcessCost: core.ProcessCost{
					ProcessTime:   ptime,
					ExecTime:      ptime - (ibs.AccountReads + ibs.StorageReads + ibs.CodeReads + ibs.IncarReads),
					StateReadCnt:  uint64(ibs.AccountReadCnt + ibs.StorageReadCnt + ibs.CodeReadCnt + ibs.IncarReadCnt),
					StateReadTime: ibs.AccountReads + ibs.StorageReads + ibs.CodeReads + ibs.IncarReads,
				},
				DetailTxMetrics: detailTxMetrics,
			}
		}
	}
}

func (s *Ethereum) txWorker(id int, task <-chan *core.TransactionTask, result chan<- *core.TransactionTaskResult) {
	var tx2 kv.Tx
	for txTask := range task {
		txn := txTask.Block.Transactions()[txTask.TxIndex]
		signer := types.MakeSigner(s.chainConfig, txTask.Block.NumberU64(), txTask.Block.Time())
		rules := s.chainConfig.Rules(txTask.Block.NumberU64(), txTask.Block.Time())
		tx, err := s.chainDB.BeginRo(context.Background())
		if err != nil {
			log.Error(err.Error())
			result <- &core.TransactionTaskResult{
				Err: err,
			}
			return
		}

		getHeader := func(hash libcommon.Hash, n uint64) *types.Header {
			h, _ := s.blockReader.HeaderByNumber(context.Background(), tx, n)
			return h
		}
		blockCtx := core.NewEVMBlockContext(txTask.Block.Header(), core.GetHashFn(txTask.Block.Header(), getHeader), s.engine, nil)
		// msg, err := TransactionToMessage(tx, signer, txTask.Block.BaseFee())

		// statedb, err := bc.GetArchiveStateDBAt(txTask.Block, txTask.TxIndex)
		if s.slimArchiveDB == nil {
			tx2, err = s.chainDB.BeginRo(context.Background())
			if err != nil {
				log.Error(err.Error())
				result <- &core.TransactionTaskResult{
					Err: err,
				}
				return
			}
		}

		ibs, err := s.ComputeTxEnv(context.Background(), txTask.Block, tx2, txTask.TxIndex)
		if err != nil {
			log.Error(err.Error())
			result <- &core.TransactionTaskResult{
				Err: err,
			}
			return
		}
		ibs.SetTxContext(txn.Hash(), txTask.Block.Hash(), txTask.TxIndex)
		msg, err := txn.AsMessage(*signer, txTask.Block.BaseFee(), rules)
		if err != nil {
			log.Error(err.Error())
			result <- &core.TransactionTaskResult{
				Err: err,
			}
			return
		}
		if msg.FeeCap().IsZero() && s.engine != nil {
			syscall := func(contract common.Address, data []byte) ([]byte, error) {
				return core.SysCallContract(contract, data, s.chainConfig, ibs, txTask.Block.Header(), s.engine, true /* constCall */)
			}
			msg.SetIsFree(s.engine.IsServiceTransaction(msg.From(), syscall))
		}

		txCtx := evmtypes.TxContext{
			TxHash:   txn.Hash(),
			Origin:   msg.From(),
			GasPrice: msg.GasPrice(),
		}

		vmenv := vm.NewEVM(blockCtx, txCtx, ibs, s.chainConfig, vm.Config{})
		var refunds = true
		_, err = core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()).AddDataGas(msg.DataGas()), refunds, false /* gasBailout */)

		if err != nil {
			log.Error(err.Error(), "block", txTask.Block.NumberU64(), "index", txTask.TxIndex)
			result <- &core.TransactionTaskResult{Err: err}
			return
		}
		if s.slimArchiveDB == nil {
			tx2.Rollback()
		}
		tx.Rollback()
		result <- &core.TransactionTaskResult{
			TransactionTask: txTask,
		}
	}
}

func (s *Ethereum) TestTxProcess(tasks []*core.TransactionTask, concurrency int) (*core.BatchTransactionTaskResult, error) {
	taskCh := make(chan *core.TransactionTask, len(tasks))
	go func() {
		for _, task := range tasks {
			taskCh <- task
		}
		close(taskCh)
	}()

	resCh := make(chan *core.TransactionTaskResult, len(tasks))
	for i := 0; i < concurrency; i++ {
		go func(id int) {
			s.txWorker(id, taskCh, resCh)
		}(i)
	}
	result := &core.BatchTransactionTaskResult{}
	var err error
	bar := progressbar.Default(int64(len(tasks)))
	for i := 0; i < len(tasks); i++ {
		bar.Add(1)
		ret := <-resCh
		if ret.Err != nil {
			return nil, err
		}
		result.TxCnt += 1
		result.GasUsed += ret.GasUsed
		result.ProcessTime += ret.ProcessTime
		// result.StatePrepareTime += ret.StatePrepareTime
	}

	return result, nil
}

func (s *Ethereum) ProcessBlocks(blocks types.Blocks, concurrency int, recreateStateDbEachTx bool) (*core.BatchBlockTaskResult, error) {
	taskCh := make(chan *types.Block, len(blocks))
	go func() {
		for _, block := range blocks {
			taskCh <- block
		}
		close(taskCh)
	}()

	// bc.SampleMidBlockTaskByGasUsed(taskCh, start, end, 1000)
	resCh := make(chan *core.BlockTaskResult, len(blocks))

	for i := 0; i < concurrency; i++ {
		go func(id int) {
			if recreateStateDbEachTx {
				s.blockTxWorker(id, taskCh, resCh)
			} else {
				s.blockWorker(id, taskCh, resCh)
			}
		}(i)
	}

	result := &core.BatchBlockTaskResult{
		BlockCnt: len(blocks),
	}

	result.TxLevelCostLookUp = core.NewTxLevelCostLookUp(core.MaxTxCnt(blocks))

	bar := progressbar.Default(int64(len(blocks)))
	for i := 0; i < len(blocks); i++ {
		bar.Add(1)
		ret := <-resCh
		if ret.Err != nil {
			return nil, ret.Err
		}
		result.TxCnt += ret.Block.Transactions().Len()
		result.GasUsed += ret.Block.GasUsed()
		result.ProcessTime += ret.ProcessTime
		result.ExecTime += ret.ExecTime
		result.StateReadTime += ret.StateReadTime
		result.StateReadCnt += ret.StateReadCnt

		for i := range ret.DetailTxMetrics {
			result.TxLevelCostLookUp.Add(i, ret.DetailTxMetrics[i].PrepareCost, ret.DetailTxMetrics[i].ProcessTime)
		}

	}

	return result, nil
}

func (s *Ethereum) TestBlockProcessV2(tasks types.Blocks, concurrency int, recreateStateDbEachTx bool) ([]*core.BlockTxReplayCost, error) {
	taskCh := make(chan *types.Block, len(tasks))
	go func() {
		for _, block := range tasks {
			taskCh <- block
		}
		close(taskCh)
	}()

	// bc.SampleMidBlockTaskByGasUsed(taskCh, start, end, 1000)
	resCh := make(chan *core.BlockTaskResult, len(tasks))

	for i := 0; i < concurrency; i++ {
		go func(id int) {
			if recreateStateDbEachTx {
				s.blockTxWorker(id, taskCh, resCh)
			} else {
				s.blockWorker(id, taskCh, resCh)
			}
		}(i)
	}

	results := make([]*core.BlockTxReplayCost, 0)
	// if bc.EnableDetailTxMetrics {
	// 	result.TxLevelCostLookUp = NewTxLevelCostLookUp(MaxTxCnt(tasks))
	// }
	bar := progressbar.Default(int64(len(tasks)))
	for i := 0; i < len(tasks); i++ {
		bar.Add(1)
		ret := <-resCh
		if ret.Err != nil {
			return nil, ret.Err
		}
		result := &core.BlockTxReplayCost{
			Number: ret.Block.NumberU64(),
		}
		for _, txMetrics := range ret.DetailTxMetrics {
			result.Txs = append(result.Txs, [2]time.Duration{txMetrics.PrepareCost, txMetrics.ProcessTime})
			if state.EnabledExpensive {
				result.TxDetailt = append(result.TxDetailt, core.TxProcessDetailMilli{
					ExecTime:      float64(txMetrics.ProcessDetail.ExecTime) / float64(time.Millisecond),
					StateReadCnt:  txMetrics.ProcessDetail.StateReadCnt,
					StateReadTime: float64(txMetrics.ProcessDetail.StateReadTime) / float64(time.Millisecond),
				})
			}
		}
		if len(result.Txs) != ret.Block.Transactions().Len() {
			return nil, fmt.Errorf("invalid metrics length for block: %v", ret.Block.NumberU64())
		}
		results = append(results, result)
	}

	return results, nil
}
